package com.review.git;

import com.review.model.DiffRequest;
import com.review.model.DiffResult;
import com.review.model.DiffResult.FileDiff;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class GitDiffService {

	private final File repoDir;

	public GitDiffService(String repoPath) {
		this.repoDir = new File(repoPath);
	}

	public DiffResult computeDiff(DiffRequest request) throws Exception {
		try (Repository repository = new FileRepositoryBuilder()
				.setGitDir(resolveGitDir(repoDir))
				.readEnvironment()
				.findGitDir(repoDir)
				.build();
			 Git git = new Git(repository)) {

			String mode = request.getMode();
			List<DiffEntry> entries = new ArrayList<>();
			List<FileDiff> out = new ArrayList<>();

			if ("staged".equalsIgnoreCase(mode)) {
				entries = diffHeadToIndex(repository, git);
			} else if ("working".equalsIgnoreCase(mode)) {
				entries = diffIndexToWorking(repository, git);
			} else if ("diff".equalsIgnoreCase(mode) || "commits".equalsIgnoreCase(mode) || "branches".equalsIgnoreCase(mode)) {
				String base = request.getBase();
				String head = request.getHead();
				entries = diffBetweenTrees(repository, git, base, head);
			} else {
				throw new IllegalArgumentException("Unknown mode: " + mode);
			}

			try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
				 DiffFormatter df = new DiffFormatter(baos)) {
				df.setRepository(repository);
				for (DiffEntry e : entries) {
					baos.reset();
					df.format(e);
					FileDiff fd = new FileDiff();
					fd.path = e.getNewPath();
					fd.changeType = e.getChangeType().name();
					fd.diff = baos.toString();
					out.add(fd);
				}
			}

			DiffResult result = new DiffResult();
			result.setMode(mode);
			result.setBase(request.getBase());
			result.setHead(request.getHead());
			result.setFiles(out);
			return result;
		}
	}

	private File resolveGitDir(File directory) {
		File gitDir = new File(directory, ".git");
		if (gitDir.exists()) return gitDir;
		return directory;
	}

	private List<DiffEntry> diffHeadToIndex(Repository repo, Git git) throws Exception {
		ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}");
		DirCache index = repo.readDirCache();
		CanonicalTreeParser headIter = new CanonicalTreeParser();
		headIter.reset(repo.newObjectReader(), headTree);
		CanonicalTreeParser indexIter = new CanonicalTreeParser();
		indexIter.reset(repo.newObjectReader(), index.writeTree(repo.newObjectInserter()));
		return git.diff().setNewTree(indexIter).setOldTree(headIter).call();
	}

	private List<DiffEntry> diffIndexToWorking(Repository repo, Git git) throws Exception {
		DirCache index = repo.readDirCache();
		CanonicalTreeParser indexIter = new CanonicalTreeParser();
		indexIter.reset(repo.newObjectReader(), index.writeTree(repo.newObjectInserter()));
		WorkingTreeIterator workTree = new FileTreeIterator(repo);
		return git.diff().setOldTree(indexIter).setNewTree(workTree).call();
	}

	private List<DiffEntry> diffBetweenTrees(Repository repo, Git git, String baseRef, String headRef) throws Exception {
		ObjectId base = repo.resolve(baseRef);
		ObjectId head = repo.resolve(headRef);
		if (base == null || head == null) throw new IllegalArgumentException("Cannot resolve refs: " + baseRef + " " + headRef);
		CanonicalTreeParser baseIter = new CanonicalTreeParser();
		baseIter.reset(repo.newObjectReader(), git.getRepository().parseCommit(base).getTree());
		CanonicalTreeParser headIter = new CanonicalTreeParser();
		headIter.reset(repo.newObjectReader(), git.getRepository().parseCommit(head).getTree());
		return git.diff().setOldTree(baseIter).setNewTree(headIter).call();
	}
}

